今天我們繼續完成剩下的部分,昨天已經完成 GET / posts
的 api,也順利接值了,今天繼續完 CREATE
DELETE
UPDATE
的部分。
昨天的 post
資料我們是透過 prisma studio
去添加的,但實際開發中我們並不會這麼做,主要是讓讀者可以大概知道 prisma studio
有什麼內容,所以接下來就來實現如何 create post
。
首先先回顧一下昨日定義的 schema
// @/validate/api/post
import { z } from "zod";
export const getPostSchema = z.object({
post_id: z.string()
})
export type GetPostSchema = z.infer<typeof getPostSchema>
export const createPostSchema = z.object({
title: z.string().min(1, { message: 'title required' }),
content: z.string().optional(),
published: z.boolean().default(false)
})
export type CreatePostSchema = z.infer<typeof createPostSchema>
export const togglePostPuPublishedSchema = z.object({
id: z.number(),
published: z.boolean()
})
export type TogglePostPuPublishedSchema = z.infer<typeof togglePostPuPublishedSchema>
export const deletePostSchema = z.object({
id: z.number()
})
export type DeletePostSchema = z.infer<typeof deletePostSchema>
之後創建 addPost
的 api
,在 trpc
中並不像 RESTful API
有各種 https methods
例如 GET
、POST
、DELETE
等等,取而代之的就是除了 GET
以外是透過 .query()
,其餘都是 .mutation()
涵蓋一切,用起來跟 graphql
一樣。
這邊的 addPost
邏輯很簡單:
post
添加到 db
,會先找尋 duplicatePost
如果有救throw error。prisma.post.create
結果返回 postid
。// ~src/server/api/post.ts
import { z } from "zod";
import { publicProcedure, router } from "./trpc";
import { TRPCError } from "@trpc/server";
import {
getPostSchema,
createPostSchema,
togglePostPuPublishedSchema,
deletePostSchema
} from "@/validate/api/post";
export const postsRouter = router({
//..
addPost: publicProcedure
.input(createPostSchema)
.mutation(async ({ input, ctx }) => {
const { prisma } = ctx
const duplicatePost = await prisma.post.findFirst({
where: {
title: input.title
}
})
if (duplicatePost) {
throw new TRPCError({ code: 'CONFLICT', message: 'Title already exists' })
}
const { id: postId } = await prisma.post.create({
data: input,
select: {
id: true
}
})
return { message: 'success create post', id: postId }
})
})
在 prisma
中可以透過 select
選擇你要 return
的 fields,這樣的寫法假如你的 data 有很多 key
可以大大減少不需要 return
的資料,除了提升 query
效能外,可讀性也有幫助。
const { id: postId } = await prisma.post.create({
data: input,
select: {
id: true
}
})
之後我們到 PostForm
引入 addPost
,api.posts.addPost.useMutation
會 return
mutateAsync
的 function
,我們在 onSubmit
把我們 form data
傳參數給他,然後在 useMutation
中還有 onSuccess
跟 onError
callback
function
,用來驗證 mutateAsync
成功與否。
// ~src/components/PostForm.tsx
import { RouterInputs, api } from '@/utils/api'
import { createPostSchema, type CreatePostSchema } from '@/validate/api/post'
import { zodResolver } from '@hookform/resolvers/zod'
import React from 'react'
import { FieldError, FieldErrors, SubmitHandler, useForm } from 'react-hook-form'
import { Input } from './Input'
import { Button } from './Button'
import { TRPCClientError } from '@trpc/client'
import { queryClient } from './Provider'
export const PostForm = () => {
const { mutateAsync: createPost } = api.posts.addPost.useMutation({
onSuccess: () => {
console.log('onSuccess')
},
onError: (e) => {
if (e instanceof TRPCClientError) {
console.log('TRPCClientError')
}
}
})
const { register, formState: { errors }, handleSubmit } = useForm<CreatePostSchema>({
resolver: zodResolver(createPostSchema),
mode: 'onChange',
defaultValues: {
published: false
}
})
const onSubmit: SubmitHandler<CreatePostSchema> = async (data) => {
await createPost(data)
}
return (
<div
className="
bg-white
px-4
py-8
shadow
sm:rounded-lg
sm:px-10
"
>
// ..
</div>
);
}
但這時你發現我明明按下 submit
,卻沒有新增 post
。
此時你重新整理畫面後資料就出現了。
但顯然這不是我們要的結果,雖然說我們成功新增 post data
到 db
,但我們 client
端得 post lists
並沒有新的內容。
原因在於 query cache 並沒有更新。
所以問題是因為 react query
得 cache
並沒有 update
內容,相信有用過 react query
的讀者肯定知道可以透過 query key
方式透過呼叫 queryClient.invalidateQueries
去更新 query cache
內容,但 trpc
有提供 useContext
整合 invalidate
功能。
import { queryClient } from './Provider'
export const PostForm = () => {
const utils = api.useContext()
const { mutateAsync: createPost } = api.posts.addPost.useMutation({
onSuccess: () => {
queryClient.invalidateQueries({
queryKey:['yourKEY']
})
}
透過 useContext
我們就可以根據 route 去 invalidate query cache 得內容摟~
export const PostForm = () => {
const utils = api.useContext()
const { mutateAsync: createPost } = api.posts.addPost.useMutation({
onSuccess: () => {
utils.posts.getPosts.invalidate()
console.log('onSuccess')
},
這樣我們每次 create post
,getPosts
內容就會重新 update
了。
trpc
invalidate 功能。如果你希望每次執行 useMutation
自動更新所有 query cache
內容,而不是一個一個根據 router
invalidate
的話你可以這樣寫。
export const trpc = createTRPCReact<AppRouter, SSRContext>({
overrides: {
useMutation: {
/**
* This function is called whenever a `.useMutation` succeeds
**/
async onSuccess(opts) {
/**
* @note that order here matters:
* The order here allows route changes in `onSuccess` without
* having a flash of content change whilst redirecting.
**/
// Calls the `onSuccess` defined in the `useQuery()`-options:
await opts.originalFn();
// Invalidate all queries in the react-query cache:
await opts.queryClient.invalidateQueries();
},
},
},
});
或是你只想在特定的 useMutation
中 invalidate all query cache
,可以這樣做
export const PostForm = () => {
const utils = api.useContext()
const { mutateAsync: createPost } = api.posts.addPost.useMutation({
onSuccess: () => {
utils.invalidate()
console.log('onSuccess')
},
剩下的 Update toggle
跟 delete post
就很簡單了,就執行 prisma.post.update
跟prisma.post.delete
function
。
import { z } from "zod";
import { publicProcedure, router } from "./trpc";
import { TRPCError } from "@trpc/server";
import {
getPostSchema,
createPostSchema,
togglePostPuPublishedSchema,
deletePostSchema
} from "@/validate/api/post";
export const postsRouter = router({
// ..
addPost: publicProcedure
.input(createPostSchema)
.mutation(async ({ input, ctx }) => {
const { prisma } = ctx
const duplicatePost = await prisma.post.findFirst({
where: {
title: input.title
}
})
if (duplicatePost) {
throw new TRPCError({ code: 'CONFLICT', message: 'Title already exists' })
}
const { id: postId } = await prisma.post.create({
data: input,
select: {
id: true
}
})
return { message: 'success create post', id: postId }
}),
togglePostPublish: publicProcedure
.input(togglePostPuPublishedSchema)
.mutation(async ({ input, ctx }) => {
const { id, published } = input
const { prisma } = ctx
await prisma.post.update({
where: {
id
},
data: {
published
}
})
}),
deletePost: publicProcedure
.input(deletePostSchema)
.mutation(async ({ input, ctx }) => {
const { id } = input
const { prisma } = ctx
await prisma.post.delete({
where: {
id
}
})
})
})
最後再把 api
加上,這樣你就完成所有 crud
功能摟~
import { PostForm } from "@/components/PostForm";
import { api } from "@/utils/api";
import { AiFillDelete } from "react-icons/ai";
export default function Home() {
const utils = api.useContext()
const { data: posts, isLoading, isError, error } = api.posts.getPosts.useQuery()
const { mutateAsync: togglePost } = api.posts.togglePostPublish.useMutation({
onSuccess: () => {
utils.posts.invalidate()
}
})
const { mutateAsync: deletePost } = api.posts.deletePost.useMutation({
onSuccess: () => {
utils.posts.invalidate()
}
})
if (isLoading) return 'isLoading'
if (isError) return error.message
return (
<div className="bg-gray-100 min-h-screen overflow-y-auto p-4">
<h2 className="text-center text-3xl">Create posts</h2>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<PostForm />
<ul className="flex flex-col gap-[1rem] justify-center mt-5">
{posts.map((post, index) => (
<li key={post.id} className="flex items-center justify-between">
<label
htmlFor=""
className={`
text-2xl
${!!post.published && "line-through"}
`}
onClick={async () => {
await togglePost({ id: post.id, published: !post.published })
}}
>{post.title}</label>
<AiFillDelete
color="red"
className="cursor-pointer"
size={20}
onClick={async () => {
await deletePost({ id: post.id })
}}
/>
</li>
))}
</ul>
</div>
</div>
);
}
完整的 demo
可以在這邊查看: https://github.com/Danny101201/next_demo/tree/main
✅ 前端社群 :
https://lihi3.cc/kBe0Y